package logger

import (
	"context"
	"errors"
	"fmt"
	"gitee.com/vrv_media/go-micro-framework/pkg/common/util/fileutil"
	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
	"net/url"
	"os"
	"runtime"
	"sort"
	"strings"
	"sync"
	"time"
)

const (
	numAttr             = 5
	KeyRequestID string = "requestID"
	KeyUsername  string = "username"
	KeyUserID    string = "userid"
)

var (
	logSeverityKey = attribute.Key("log.severity")
	logMessageKey  = attribute.Key("log.message")
	logTemplateKey = attribute.Key("log.template")

	_encoderMutex sync.RWMutex
)

// ZapLogger log的zap实现类，推荐调用NewZapLog方法创建
type ZapLogger struct {
	*zap.Logger

	sugarLogger *zap.SugaredLogger

	outWriteSyncer   zapcore.WriteSyncer
	errorWriteSyncer zapcore.WriteSyncer

	withTraceID bool

	minLevel         zapcore.Level
	errorStatusLevel zapcore.Level

	caller     bool
	stackTrace bool
	// 使用sugar打印日志，默认开启
	withSugar bool

	// extraFields contains a number of zap.Fields that are added to every log entry
	extraFields []zap.Field
}

type ZapLoggerOption func(*ZapLogger)

func WithSugar(on bool) ZapLoggerOption {
	return func(l *ZapLogger) {
		l.withSugar = on
	}
}
func WithCaller(on bool) ZapLoggerOption {
	return func(l *ZapLogger) {
		l.caller = on
	}
}
func WithStackTrace(on bool) ZapLoggerOption {
	return func(l *ZapLogger) {
		l.stackTrace = on
	}
}

func NewZapLog(opts *Options, zapOptions ...ZapLoggerOption) *ZapLogger {
	if opts == nil {
		opts = NewOptions() // 如果不传递 opts 则使用默认创建
	} else {
		if err := opts.Validate(); err != nil {
			panic(err)
		}
	}
	var zapLevel zapcore.Level
	// 获取 opts.Level 等级 需要转化一下
	if err := zapLevel.UnmarshalText([]byte(opts.Level)); err != nil {
		zapLevel = zapcore.InfoLevel
	}
	// 日志文件路径如果是相对路径，需要转成绝对路径
	replaceFileOutPutPath(opts)

	encodeLevel := zapcore.CapitalLevelEncoder //将Level序列化为全大写字符串。例如, InfoLevel被序列化为INFO
	// 当输出到本地路径时，禁止带颜色
	if opts.Format == consoleFormat && opts.EnableColor {
		encodeLevel = zapcore.CapitalColorLevelEncoder // 将Level序列化为全大写字符串并添加颜色。例如，InfoLevel被序列化为INFO，并被标记为蓝色。
	}
	encoderConfig := zapcore.EncoderConfig{ // 设置 编码器
		TimeKey:        "timestamp",
		LevelKey:       "level",
		NameKey:        "logger",
		CallerKey:      "caller",
		MessageKey:     "message",
		StacktraceKey:  "stacktrace",
		LineEnding:     zapcore.DefaultLineEnding,   // 每行结束 换行
		EncodeLevel:    encodeLevel,                 // log 等级
		EncodeTime:     timeEncoder,                 // 时间编码器
		EncodeDuration: milliSecondsDurationEncoder, // 把时间戳设置成毫秒
		EncodeCaller:   zapcore.ShortCallerEncoder,  // 行调用编码器
	}

	loggerConfig := &zap.Config{
		Level:             zap.NewAtomicLevelAt(zapLevel), // logger 启动的最小级别
		Development:       opts.Development,               // 是否是开发模式
		DisableCaller:     opts.DisableCaller,             // 停止使用调用函数的文件名和行号注释日志, 在默认情况下 所有日志都有注释
		DisableStacktrace: opts.DisableStacktrace,         // 禁用自动捕获堆栈跟踪。默认情况下，在 dev 环境中捕获WarnLevel及以上级别的日志，在 pro 环境中捕获ErrorLevel及以上级别的日志
		Sampling: &zap.SamplingConfig{ // 设置采样策略。nil 禁用采样。
			Initial:    100,
			Thereafter: 100,
		},
		Encoding:         opts.Format, // 设置记录器的编码 有效值 console 和 json
		EncoderConfig:    encoderConfig,
		OutputPaths:      opts.OutputPaths,      // 输出 默认输出到标准输出
		ErrorOutputPaths: opts.ErrorOutputPaths, // 错误输出 默认输出到标准错误
	}
	// 如果是windows平台，需要注册windows的解析器
	if runtime.GOOS == "windows" {
		_ = zap.RegisterSink("winfile", newWinFileSink)
	}

	l, err, outWriteSyncer, errorWriteSyncer := generateZapLogger(loggerConfig, opts, zap.AddStacktrace(zapcore.PanicLevel), zap.AddCallerSkip(2))
	if err != nil {
		panic(err)
	}
	logger := &ZapLogger{
		//我们希望输出的文件名和行号是调用封装函数的位置。这时可以使用zap.AddCallerSkip(skip int)向上跳 1 层：
		Logger:           l.WithOptions(zap.AddCallerSkip(1)),
		sugarLogger:      l.Sugar(),
		minLevel:         zapLevel,
		errorStatusLevel: zap.ErrorLevel,
		withTraceID:      true, // 打印log时 额外打印 trace_id 信息
		withSugar:        true,
		outWriteSyncer:   outWriteSyncer,
		errorWriteSyncer: errorWriteSyncer,
	}
	if len(zapOptions) != 0 {
		for _, opt := range zapOptions {
			opt(logger)
		}
	}
	/*
		当应用程序中同时使用了 log 模块和 zap 日志库时，如果不对两者进行整合，就可能会出现一些问题：
			1. 日志信息不统一：log 模块的日志输出格式和 zap 日志库的输出格式可能不一致，导致在分析和处理日志时比较麻烦。
			2. 日志信息遗漏：如果不将 log 模块的日志信息也输入到 zap 日志库中，就可能会出现部分日志信息遗漏的情况。
			3. 日志信息错乱：如果将 log 模块的日志信息和 zap 日志库的信息分别输出到不同的文件或控制台中，就可能会出现信息错乱或混杂的情况。
		因此，我们可以使用 zap.RedirectStdLog 方法将 log 模块的日志信息重定向到 zap 日志库中，从而避免上述问题。
		这个方法的作用是将 log 模块的输出重定向到 zap 日志库中，从而使得两者的日志信息都能够被 zap 日志库捕获和处理，同时也能够保证日志信息的统一、完整和正确输出。
	*/
	zap.RedirectStdLog(l)

	// 将fmt的日志也打印到zap当中
	redirectStdoutToZap(logger)

	return logger
}

// 重定向标准输出到 Zap
func redirectStdoutToZap(l *ZapLogger) {
	// 创建管道：读取端（r）和写入端（w）
	r, w, _ := os.Pipe()
	os.Stdout = w // 将标准输出重定向到管道的写入端

	// 异步读取管道内容并记录到 Zap
	go func() {
		defer func(r *os.File) {
			_ = r.Close()
		}(r)
		buf := make([]byte, 10240)
		for {
			n, err := r.Read(buf)
			if err != nil || n == 0 {
				return
			}
			msg := string(buf[:n])
			l.InfoF("[fmt redirect]: %s", msg)
		}
	}()
}
func replaceFileOutPutPath(opts *Options) {
	if len(opts.OutputPaths) > 0 {
		opts.OutputPaths = replaceToRedirectPath(opts.OutputPaths)
	}
	if len(opts.ErrorOutputPaths) > 0 {
		opts.ErrorOutputPaths = replaceToRedirectPath(opts.ErrorOutputPaths)
	}

}

func replaceToRedirectPath(paths []string) []string {
	if len(paths) == 0 {
		return []string{}
	}
	redirectPath := make([]string, 0, len(paths))
	for _, path := range paths {
		if strings.HasPrefix(path, "./") {
			path = strings.Replace(path, "./", fileutil.GetCurrentDirectory(), 1)
			if runtime.GOOS == "windows" {
				path = "winfile:///" + path
			}
		}
		redirectPath = append(redirectPath, path)
	}
	return redirectPath
}

func generateZapLogger(loggerConfig *zap.Config, opts *Options, zapOpts ...zap.Option) (log *zap.Logger, err error,
	outWriteSyncer zapcore.WriteSyncer, errorWriteSyncer zapcore.WriteSyncer) {
	// 不使用文件分割，直接用原来的即可
	if !opts.EnableFileSegment {
		log, err = loggerConfig.Build(zapOpts...)
		return log, err, nil, nil
	}
	// 如果不适用文件分割和输出文件中没有文件路径，只有标准输出，则只需要创建默认的即可
	stdOutPaths, fileOutPaths := segmentStdAndFileOutPut(opts.OutputPaths)
	if len(stdOutPaths) == 0 && len(fileOutPaths) == 0 {
		return nil, fmt.Errorf("output path is empty"), nil, nil
	}
	errorStdPath, errFilePaths := segmentStdAndFileOutPut(opts.ErrorOutputPaths)
	if len(errorStdPath) == 0 && len(errFilePaths) == 0 {
		return nil, fmt.Errorf("error output path is empty"), nil, nil
	}
	if len(fileOutPaths) == 0 && len(errFilePaths) == 0 {
		log, err = loggerConfig.Build(zapOpts...)
		return log, err, nil, nil
	}
	// 如果有输出文件路径，则需要创建多个输出文件
	outWriteSyncer, err = generateStdAndFileWriteSyncer(stdOutPaths, fileOutPaths, opts)
	if err != nil {
		return nil, err, nil, nil
	}
	errorWriteSyncer, err = generateStdAndFileWriteSyncer(errorStdPath, errFilePaths, opts)
	if err != nil {
		return nil, err, nil, nil
	}
	// 生成encoder对象
	encoder, err := newEncoder(loggerConfig.Encoding, loggerConfig.EncoderConfig)
	if err != nil {
		return nil, err, nil, nil
	}
	if loggerConfig.Level == (zap.AtomicLevel{}) {
		return nil, errors.New("missing Level"), nil, nil
	}
	log = zap.New(
		zapcore.NewCore(encoder, outWriteSyncer, loggerConfig.Level),
		buildErrOptions(errorWriteSyncer, loggerConfig)...,
	)
	if len(zapOpts) > 0 {
		log = log.WithOptions(zapOpts...)
	}

	return log, nil, outWriteSyncer, errorWriteSyncer

}

func buildErrOptions(errSink zapcore.WriteSyncer, cfg *zap.Config) []zap.Option {
	opts := []zap.Option{zap.ErrorOutput(errSink)}

	if cfg.Development {
		opts = append(opts, zap.Development())
	}

	if !cfg.DisableCaller {
		opts = append(opts, zap.AddCaller())
	}

	stackLevel := zap.ErrorLevel
	if cfg.Development {
		stackLevel = zap.WarnLevel
	}
	if !cfg.DisableStacktrace {
		opts = append(opts, zap.AddStacktrace(stackLevel))
	}

	if scfg := cfg.Sampling; scfg != nil {
		opts = append(opts, zap.WrapCore(func(core zapcore.Core) zapcore.Core {
			var samplerOpts []zapcore.SamplerOption
			if scfg.Hook != nil {
				samplerOpts = append(samplerOpts, zapcore.SamplerHook(scfg.Hook))
			}
			return zapcore.NewSamplerWithOptions(
				core,
				time.Second,
				cfg.Sampling.Initial,
				cfg.Sampling.Thereafter,
				samplerOpts...,
			)
		}))
	}

	if len(cfg.InitialFields) > 0 {
		fs := make([]zap.Field, 0, len(cfg.InitialFields))
		keys := make([]string, 0, len(cfg.InitialFields))
		for k := range cfg.InitialFields {
			keys = append(keys, k)
		}
		sort.Strings(keys)
		for _, k := range keys {
			fs = append(fs, zap.Any(k, cfg.InitialFields[k]))
		}
		opts = append(opts, zap.Fields(fs...))
	}
	return opts
}

func newEncoder(name string, encoderConfig zapcore.EncoderConfig) (zapcore.Encoder, error) {

	if name == "" {
		return nil, fmt.Errorf("encoder name is empty")
	}

	switch strings.ToLower(name) {
	case "console":
		return zapcore.NewConsoleEncoder(encoderConfig), nil
	case "json":
		return zapcore.NewJSONEncoder(encoderConfig), nil
	default:
		return nil, fmt.Errorf("not support encoder name :%v , must in [%v,%v]", name, "console", "json")
	}

}

func generateStdAndFileWriteSyncer(stdOutPaths, fileOutPaths []string, opts *Options) (zapcore.WriteSyncer, error) {
	stdWriteSyncer, err := generateStdWriteSyncer(stdOutPaths)
	if err != nil {
		return nil, err
	}
	fileSegmentWriteSyncer, err := generateFileSegmentWriteSyncer(fileOutPaths, opts)
	if err != nil {
		return nil, err
	}
	if stdWriteSyncer == nil && fileSegmentWriteSyncer == nil {
		return nil, fmt.Errorf("generate out writeSyncer empty")
	}
	if stdWriteSyncer == nil {
		return fileSegmentWriteSyncer, nil
	}
	if fileSegmentWriteSyncer == nil {
		return stdWriteSyncer, nil
	}
	return zap.CombineWriteSyncers(stdWriteSyncer, fileSegmentWriteSyncer), nil
}

func generateFileSegmentWriteSyncer(fileOutPaths []string, opts *Options) (zapcore.WriteSyncer, error) {
	if len(fileOutPaths) == 0 {
		return nil, nil
	}
	fileSyncers := make([]zapcore.WriteSyncer, 0)
	segmentConfig := opts.FileSegment
	for _, path := range fileOutPaths {
		// 因为用的是lumber，这里要将winfile去掉
		if strings.HasPrefix(path, "winfile:///") {
			path = strings.Replace(path, "winfile:///", "", 1)
		}
		lumberJackLogger := &lumberjack.Logger{
			Filename:   path,                     // 文件位置
			MaxSize:    segmentConfig.MaxSize,    // 进行切割之前,日志文件的最大大小(MB为单位)
			MaxAge:     segmentConfig.MaxAge,     // 保留旧文件的最大天数
			MaxBackups: segmentConfig.MaxBackups, // 保留旧文件的最大个数
			Compress:   false,                    // 是否压缩/归档旧文件
		}
		fileSyncers = append(fileSyncers, zapcore.AddSync(lumberJackLogger))
	}
	return zap.CombineWriteSyncers(fileSyncers...), nil
}

func generateStdWriteSyncer(stdOutPaths []string) (zapcore.WriteSyncer, error) {
	if len(stdOutPaths) == 0 {
		return nil, nil
	}
	writer, closeOut, err := zap.Open(stdoutPath)
	if err != nil {
		closeOut()
		return nil, err
	}
	return writer, nil
}

// 从输出中拆分出std的输出和文件的输出
func segmentStdAndFileOutPut(outputs []string) (stdOutputs []string, fileOutputs []string) {
	if len(outputs) == 0 {
		return
	}
	for _, output := range outputs {
		// 如果是文件路径，是区分大小写的，不能变，要用临时变量
		temp := strings.Trim(output, " ")
		if temp == "" {
			continue
		}
		temp = strings.ToLower(temp)
		if temp == stdoutPath || temp == stderrPath {
			stdOutputs = append(stdOutputs, temp)
		} else {
			fileOutputs = append(fileOutputs, output)
		}
	}
	return
}

// WithOptions clones the current Logger, applies the supplied Options,
// and returns the resulting Logger. It's safe to use concurrently.
func (l *ZapLogger) WithOptions(opts ...zap.Option) *ZapLogger {
	extraFields := []zap.Field{}
	// zap.New side effect is extracting fields from .WithOptions(zap.Fields(...))
	zap.New(&fieldExtractorCore{extraFields: &extraFields}, opts...)
	clone := *l
	clone.Logger = l.Logger.WithOptions(opts...)
	clone.extraFields = append(clone.extraFields, extraFields...)
	return &clone
}

func (l *ZapLogger) Flush() {
	_ = l.Logger.Sync()
	_ = l.sugarLogger.Sync()
}

func (l *ZapLogger) logFields(ctx context.Context, lvl zapcore.Level, msg string) []zapcore.Field {
	var fields []zapcore.Field = make([]zapcore.Field, 0)
	if lvl < l.minLevel || ctx == nil {
		return fields
	}
	switch ctx.(type) {
	case *gin.Context: // gin 的 context 与 普通的context 不太一样  gin 的内容存储在 ctx.Request.Context

		if requestID := ctx.Value(KeyRequestID); requestID != nil {
			if value, ok := requestID.(string); ok && value != "" {
				fields = append(fields, zap.String(KeyRequestID, value))
			}
		}
		if username := ctx.Value(KeyUsername); username != nil {
			if value, ok := username.(string); ok && value != "" {
				fields = append(fields, zap.String(KeyUsername, value))
			}
		}
		ctx = ctx.(*gin.Context).Request.Context()
	}
	// 从 context 中获取 span
	span := trace.SpanFromContext(ctx)
	// 判断 span 存在 如果不存在 返回
	if !span.IsRecording() {
		return fields
	}
	attrs := make([]attribute.KeyValue, 0, numAttr+len(fields)+len(l.extraFields))
	for _, f := range fields {
		if f.Type == zapcore.NamespaceType {
			// should this be a prefix?
			continue
		}
		attrs = appendField(attrs, f)
	}

	for _, f := range l.extraFields {
		if f.Type == zapcore.NamespaceType {
			// should this be a prefix?
			continue
		}
		attrs = appendField(attrs, f)
	}
	// 设置链路追踪相关代码
	l.log(span, lvl, msg, attrs)

	// 是否在log中打印 TraceID
	if l.withTraceID {
		traceID := span.SpanContext().TraceID().String()
		fields = append(fields, zap.String("trace_id", traceID))
	}

	return fields
}
func (l *ZapLogger) log(span trace.Span, lvl zapcore.Level, msg string, attrs []attribute.KeyValue) {
	attrs = append(attrs, logSeverityKey.String(levelString(lvl)))
	attrs = append(attrs, logMessageKey.String(msg))

	if l.caller {
		// 这里 向上跳跃5层调用栈信息 返回最初始调用 此函数的地方
		if fn, file, line, ok := runtimeCaller(5); ok {
			if fn != "" {
				attrs = append(attrs, semconv.CodeFunctionKey.String(fn))
			}
			if file != "" {
				attrs = append(attrs, semconv.CodeFilepathKey.String(file))
				attrs = append(attrs, semconv.CodeLineNumberKey.Int(line))
			}
		}
	}

	if l.stackTrace {
		// 作用 返回调用栈信息
		stackTrace := make([]byte, 2048)
		// 获取当前 Goroutine 的调用栈信息，并将其写入到给定的缓冲区中
		// buf 是一个缓冲区，用于存储调用栈信息，all 参数用于控制是否输出所有 Goroutine 的调用栈信息
		// 函数返回值是写入缓冲区的字节数。
		n := runtime.Stack(stackTrace, false)
		attrs = append(attrs, semconv.ExceptionStacktraceKey.String(string(stackTrace[0:n])))
	}

	span.AddEvent("log", trace.WithAttributes(attrs...))

	// 如果 当前log等级 大于 error span设置error状态码
	if lvl >= l.errorStatusLevel {
		span.SetStatus(codes.Error, msg)
	}
}
func runtimeCaller(skip int) (fn, file string, line int, ok bool) {
	rpc := make([]uintptr, 1)
	// 这里 +1 的原因是 当前函数也算一层栈信息
	n := runtime.Callers(skip+1, rpc[:])
	if n < 1 {
		return
	}
	frame, _ := runtime.CallersFrames(rpc).Next()
	return frame.Function, frame.File, frame.Line, frame.PC != 0
}

func newWinFileSink(u *url.URL) (zap.Sink, error) {
	// Remove leading slash left by url.Parse()
	var name string
	if u.Path != "" {
		name = u.Path[1:]
	} else if u.Opaque != "" {
		name = u.Opaque[1:]
	} else {
		return nil, errors.New("path error")
	}
	return os.OpenFile(name, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
}
